<?php
/**
 * Custom XML parser for signed and/or encrypted XML Docs
 *
 * @author  Donal McMullan  donal@catalyst.net.nz
 * @version 0.0.1
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 * @package mnet
 */

/**
 * Custom XML parser class for signed and/or encrypted XML Docs
 */
class mnet_encxml_parser {
	/**
	 * Constructor creates and initialises parser resource and calls initialise
	 *
	 * @return bool True
	 */
	function mnet_encxml_parser() {
		return $this->initialise();
	}

	/**
	 * Set default element handlers and initialise properties to empty.
	 *
	 * @return bool True
	 */
	function initialise() {
		$this->parser = xml_parser_create();
		xml_set_object($this->parser, $this);

		xml_set_element_handler($this->parser, "start_element", "end_element");
		xml_set_character_data_handler($this->parser, "discard_data");

		$this->tag_number		= 0; // Just a unique ID for each tag
		$this->digest			= '';
		$this->remote_timestamp  = '';
		$this->remote_wwwroot	= '';
		$this->signature		 = '';
		$this->data_object	   = '';
		$this->key_URI		   = '';
		$this->payload_encrypted = false;
		$this->cipher			= array();
		$this->error			 = array();
		$this->remoteerror	   = null;
		$this->errorstarted	  = false;
		return true;
	}

	/**
	 * Parse a block of XML text
	 *
	 * The XML Text will be an XML-RPC request which is wrapped in an XML doc
	 * with a signature from the sender. This envelope may be encrypted and
	 * delivered within another XML envelope with a symmetric key. The parser
	 * should first decrypt this XML, and then place the XML-RPC request into
	 * the data_object property, and the signature into the signature property.
	 *
	 * See the W3C's {@link http://www.w3.org/TR/xmlenc-core/ XML Encryption Syntax and Processing}
	 * and {@link http://www.w3.org/TR/2001/PR-xmldsig-core-20010820/ XML-Signature Syntax and Processing}
	 * guidelines for more detail on the XML.
	 *
	 * -----XML-Envelope---------------------------------
	 * |												|
	 * |	Symmetric-key--------------------------	 |
	 * |	|_____________________________________|	 |
	 * |												|
	 * |	Encrypted data-------------------------	 |
	 * |	|									 |	 |
	 * |	|  -XML-Envelope------------------	|	 |
	 * |	|  |							 |	|	 |
	 * |	|  |  --Signature-------------   |	|	 |
	 * |	|  |  |______________________|   |	|	 |
	 * |	|  |							 |	|	 |
	 * |	|  |  --Signed-Payload--------   |	|	 |
	 * |	|  |  |					  |   |	|	 |
	 * |	|  |  |   XML-RPC Request	|   |	|	 |
	 * |	|  |  |______________________|   |	|	 |
	 * |	|  |							 |	|	 |
	 * |	|  |_____________________________|	|	 |
	 * |	|_____________________________________|	 |
	 * |												|
	 * |________________________________________________|
	 *
	 * @uses $MNET
	 * @param   string  $data   The XML that you want to parse
	 * @return  bool			True on success - false on failure
	 */
	function parse($data) {
		global $MNET, $MNET_REMOTE_CLIENT;

		$p = xml_parse($this->parser, $data);

		if ($p == 0) {
			// Parse failed
			$errcode = xml_get_error_code($this->parser);
			$errstring = xml_error_string($errcode);
			$lineno = xml_get_current_line_number($this->parser);
			if ($lineno !== false) {
				$error = array('lineno' => $lineno);
				$lineno--; // Line numbering starts at 1.
				while ($lineno > 0) {
					$data = strstr($data, "\n");
					$lineno--;
				}
				$data .= "\n"; // In case there's only one line (no newline)
				$line = substr($data, 0, strpos($data, "\n"));
				$error['code']   = $errcode;
				$error['string'] = $errstring;
				$error['line']   = $line;
				$this->error[] = $error;
			} else {
				$this->error[] = array('code' => $errcode, 'string' => $errstring);
			}
		}

		if (!empty($this->remoteerror)) {
			return false;
		}

		if (count($this->cipher) > 0) {
			$this->cipher = array_values($this->cipher);
			$this->payload_encrypted = true;
		}

		return (bool)$p;
	}

	/**
	 * Destroy the parser and free up any related resource.
	 */
	function free_resource() {
		$free = xml_parser_free($this->parser);
	}

	/**
	 * Set the character-data handler to the right function for each element
	 *
	 * For each tag (element) name, this function switches the character-data
	 * handler to the function that handles that element. Note that character
	 * data is referred to the handler in blocks of 1024 bytes.
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $name   The name of the tag, e.g. method_call
	 * @param   array   $attrs  The tag's attributes (if any exist).
	 * @return  bool			True
	 */
	function start_element($parser, $name, $attrs) {
		$this->tag_number++;
		$handler = 'discard_data';
		switch(strtoupper($name)) {
			case 'DIGESTVALUE':
				$handler = 'parse_digest';
				break;
			case 'SIGNATUREVALUE':
				$handler = 'parse_signature';
				break;
			case 'OBJECT':
				$handler = 'parse_object';
				break;
			case 'RETRIEVALMETHOD':
				$this->key_URI = $attrs['URI'];
				break;
			case 'TIMESTAMP':
				$handler = 'parse_timestamp';
				break;
			case 'WWWROOT':
				$handler = 'parse_wwwroot';
				break;
			case 'CIPHERVALUE':
				$this->cipher[$this->tag_number] = '';
				$handler = 'parse_cipher';
				break;
			case 'FAULT':
				$handler = 'parse_fault';
			default:
				break;
		}
		xml_set_character_data_handler($this->parser, $handler);
		return true;
	}

	/**
	 * Add the next chunk of character data to the remote_timestamp string
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $data   The content of the current tag (1024 byte chunk)
	 * @return  bool			True
	 */
	function parse_timestamp($parser, $data) {
		$this->remote_timestamp .= $data;
		return true;
	}

	/**
	 * Add the next chunk of character data to the cipher string for that tag
	 *
	 * The XML parser calls the character-data handler with 1024-character
	 * chunks of data. This means that the handler may be called several times
	 * for a single tag, so we use the concatenate operator (.) to build the
	 * tag content into a string.
	 * We should not encounter more than one of each tag type, except for the
	 * cipher tag. We will often see two of those. We prevent the content of
	 * these two tags being concatenated together by counting each tag, and
	 * using its 'number' as the key to an array of ciphers.
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $data   The content of the current tag (1024 byte chunk)
	 * @return  bool			True
	 */
	function parse_cipher($parser, $data) {
		$this->cipher[$this->tag_number] .= $data;
		return true;
	}

	/**
	 * Add the next chunk of character data to the remote_wwwroot string
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $data   The content of the current tag (1024 byte chunk)
	 * @return  bool			True
	 */
	function parse_wwwroot($parser, $data) {
		$this->remote_wwwroot .= $data;
		return true;
	}

	/**
	 * Add the next chunk of character data to the digest string
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $data   The content of the current tag (1024 byte chunk)
	 * @return  bool			True
	 */
	function parse_digest($parser, $data) {
		$this->digest .= $data;
		return true;
	}

	/**
	 * Add the next chunk of character data to the signature string
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $data   The content of the current tag (1024 byte chunk)
	 * @return  bool			True
	 */
	function parse_signature($parser, $data) {
		$this->signature .= $data;
		return true;
	}

	/**
	 * Add the next chunk of character data to the data_object string
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $data   The content of the current tag (1024 byte chunk)
	 * @return  bool			True
	 */
	function parse_object($parser, $data) {
		$this->data_object .= $data;
		return true;
	}

	/**
	 * Discard the next chunk of character data
	 *
	 * This is used for tags that we're not interested in.
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $data   The content of the current tag (1024 byte chunk)
	 * @return  bool			True
	 */
	function discard_data($parser, $data) {
		if (!$this->errorstarted) {
			// Not interested
			return true;
		}
		$data = trim($data);
		if (isset($this->errorstarted->faultstringstarted) && !empty($data)) {
			$this->remoteerror .= ', message: ' . $data;
		} else if (isset($this->errorstarted->faultcodestarted)) {
			$this->remoteerror = 'code: ' . $data;
			unset($this->errorstarted->faultcodestarted);
		} else if ($data == 'faultCode') {
			$this->errorstarted->faultcodestarted = true;
		} else if ($data == 'faultString') {
			$this->errorstarted->faultstringstarted = true;
		}
		return true;

	}

	function parse_fault($parser, $data) {
		$this->errorstarted = new StdClass;
		return true;
	}

	/**
	 * Switch the character-data handler to ignore the next chunk of data
	 *
	 * @param   mixed   $parser The XML parser
	 * @param   string  $name   The name of the tag, e.g. method_call
	 * @return  bool			True
	 */
	function end_element($parser, $name) {
		$ok = xml_set_character_data_handler($this->parser, "discard_data");
		return true;
	}
}
?>
